﻿namespace Legend.Web.QueryString
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.Serialization;
    using Legend.Collections;
    using Legend.Reflection;
    using Legend.Threading;

    /// <summary>
    /// Provides access to the members of the data contract that
    /// the type specified in the constructor adheres to.
    /// </summary>
    public class DataContractMemberAccessor : ISerializableMemberAccessor
    {
        private const BindingFlags ContractMemberBindingFlags =
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

        private static SynchronizedDictionary<Type, ContractTypeInfo> validContracts = new Dictionary<Type, ContractTypeInfo>().AsSynchronized();
        private ContractTypeInfo currentTypeInfo;

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="contractType">The type to access the contract members of.</param>
        public DataContractMemberAccessor(Type contractType)
        {
            Require.ThatArgument(contractType).Named("contractType").IsNotNull();

            var contractTypeInfo = GetContractTypeInfo(contractType);

            if (!contractTypeInfo.IsValidContractType)
            {
                throw new InvalidOperationException("The specified type is not a valid data contract.");
            }

            this.currentTypeInfo = contractTypeInfo;
        }

        private static ContractTypeInfo GetContractTypeInfo(Type contractType)
        {
            ContractTypeInfo typeInfo;

            using (var readLock = validContracts.AcquireUpgradableReadLock())
            {
                if (!validContracts.TryGetValue(contractType, out typeInfo))
                {
                    typeInfo = new ContractTypeInfo();
                    typeInfo.IsValidContractType =
                        contractType.GetCustomAttributes(typeof (DataContractAttribute), true).Length > 0;

                    if (typeInfo.IsValidContractType)
                    {
                        typeInfo.CachedMembers = GetContractMembers(contractType);
                    }


                    using (readLock.Upgrade())
                    {
                        validContracts.Add(contractType, typeInfo);
                    }
                }
            }

            return typeInfo;
        }

        private static SerializableMemberInfoCollection GetContractMembers(Type contractType)
        {
            var taggedMembers =
                from member in contractType.GetMembers(ContractMemberBindingFlags)
                from attribute in member.GetCustomAttributes(typeof(DataMemberAttribute), true).Cast<DataMemberAttribute>()
                let valueMember = ValueMemberInfo.Create(member)
                select new SerializableMemberInfo(GetContractMemberName(member, attribute), GetTypeConverter(valueMember), valueMember, attribute.Order);


            return new SerializableMemberInfoCollection(taggedMembers);
        }

        private static TypeConverter GetTypeConverter(ValueMemberInfo member)
        {
            TypeConverterAttribute converterAttribute =
                (from attribute in member.GetCustomAttributes(typeof(TypeConverterAttribute), true).Cast<TypeConverterAttribute>()
                 select attribute).SingleOrDefault();

            TypeConverter converter;

            if (converterAttribute != null)
            {
                converter = (TypeConverter)Activator.CreateInstance(Type.GetType(converterAttribute.ConverterTypeName));
            }
            else
            {
                converter = TypeDescriptor.GetConverter(member.ValueType);
            }

            return converter;
        }

        /// <summary>
        /// Gets the contract members from the specified type.
        /// </summary>
        /// <returns>A collection of the contract members.</returns>
        public SerializableMemberInfoCollection GetSerializableMembers()
        {
            return this.currentTypeInfo.CachedMembers;
        }

        private static string GetContractMemberName(MemberInfo member, DataMemberAttribute attribute)
        {
            return attribute.Name ?? member.Name;
        }

        private class ContractTypeInfo
        {
            public bool IsValidContractType;
            public SerializableMemberInfoCollection CachedMembers;
        }
    }
}
